package net.caidingke.shepherd.service;

import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import io.ebean.Ebean;
import io.ebean.EbeanServer;
import io.ebean.PagedList;
import io.ebean.annotation.Transactional;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.caidingke.shepherd.domain.user.*;
import net.caidingke.shepherd.domain.user.request.PermissionRequest;
import net.caidingke.shepherd.domain.user.request.UserRequest;
import net.caidingke.shepherd.domain.user.request.UserRoleRequest;
import net.caidingke.shepherd.exception.BusinessException;
import net.caidingke.shepherd.exception.ErrorCode;
import net.caidingke.shepherd.utils.mapper.BeanUtils;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.remoting.RemoteAccessException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * @author bowen.
 */
@Service
@Slf4j
@AllArgsConstructor
public class UserService {

    private final PasswordEncoder passwordEncoder;

    @Transactional(rollbackFor = Exception.class)
    public User register(UserRequest request) {
        Optional<User> exists = this.findByUsername(Strings.nullToEmpty(request.getUsername().trim()));
        if (exists.isPresent()) {
            throw new BusinessException(ErrorCode.USERNAME_EXISTS);
        }
        User user = BeanUtils.convert(request, User.class);
        user.setPassword(passwordEncoder.encode(request.getPassword()));
        user.setRegistered(LocalDateTime.now());
        user.setCityId(request.getCityId());
        user.insert();
        if (CollectionUtils.isNotEmpty(request.getRoleIds())) {
            for (Long roleId : request.getRoleIds()) {
                UserRole userRole = new UserRole();
                userRole.setUserId(user.getId());
                userRole.setRoleId(roleId);
                userRole.insert();
            }
        }
        return user;
    }

    @Transactional(rollbackFor = Exception.class)
    public Optional<User> findByUsername(String username) {
        return User.F.where().username.eq(username).findOneOrEmpty();
    }

    @Transactional(rollbackFor = Exception.class)
    public PagedList<User> findUsers(int page, int pageSize) {
        return Ebean.find(User.class)
                .setFirstRow(page * pageSize)
                .setMaxRows(pageSize)
                .findPagedList();
    }

    @Transactional(rollbackFor = Exception.class)
    @Cacheable(cacheNames = "user")
    public Optional<User> byId(Long id) {
        return Optional.ofNullable(User.F.byId(id));
    }

    @Transactional(rollbackFor = Exception.class)
    public Optional<User> modifyPassword(User user, Long userId, String password) {
        if (!Objects.equals(user.getId(), userId) && !this.hasRole(user.getId(), Role.ADMIN)
                && !this.hasPermission(user.getId(), Permission.MODIFY_PASSWORD)) {
            throw new BusinessException(ErrorCode.NO_PERMISSION);
        }
        Optional<User> existsUser = byId(userId);
        existsUser.map(u -> {
            u.setPassword(passwordEncoder.encode(password));
            u.update();
            return u;
        });
        return existsUser;
    }

    @Transactional(rollbackFor = Exception.class)
    public List<Role> findRoles(User user) {
        // TODO: 2017/11/16 判断是否具有获取所有角色的权限
        return Role.F.where().findList();
    }

    @Transactional(rollbackFor = Exception.class)
    public Optional<Role> findRoleById(Long id) {
        return Optional.ofNullable(Role.F.byId(id));
    }

    @Transactional(rollbackFor = Exception.class)
    public void saveRole(UserRoleRequest request) {
        Role role = BeanUtils.convert(request, Role.class);
        role.insert();
        if (CollectionUtils.isNotEmpty(request.getPermissionIds())) {
            for (Long permissionId : request.getPermissionIds()) {
                RolePermission rolePermission = new RolePermission();
                rolePermission.setRoleId(role.getId());
                rolePermission.setPermissionId(permissionId);
                rolePermission.insert();
            }
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public void modifyRolePermission(Long roleId, List<Long> permissions) {
        RolePermission.F.where().roleId.eq(roleId).delete();
        if (CollectionUtils.isNotEmpty(permissions)) {
            for (Long id : permissions) {
                RolePermission permission = new RolePermission();
                permission.setRoleId(roleId);
                permission.setPermissionId(id);
                permission.insert();
            }
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public List<Permission> findPermissions() {
        return Permission.F.where().findList();
    }

    @Transactional(rollbackFor = Exception.class)
    public void permission(PermissionRequest request) {
        Permission permission = BeanUtils.convert(request, Permission.class);
        permission.insert();
    }

    @Transactional(rollbackFor = Exception.class)
    public Optional<Permission> findPermissionById(Long id) {
        return Optional.ofNullable(Permission.F.byId(id));
    }

    @Transactional(rollbackFor = Exception.class)
    public void modifyPermission(Long id, PermissionRequest request) {
        Permission permission = Permission.F.byId(id);
        if (permission == null) {
            throw new BusinessException(ErrorCode.NO_EXISTS_PERMISSION);
        }
        BeanUtils.copyProperties(request, permission);
        permission.update();
    }

    @Retryable(value = {RemoteAccessException.class}, maxAttempts = 5, backoff = @Backoff(delay = 5000L, multiplier = 1))
    public void retryTest() throws Exception {
        System.out.println("do something...");
        throw new RemoteAccessException("RemoteAccessException....");
    }

    @Recover
    public void recover(RemoteAccessException e) {
        log.error(e.getMessage(), e);
        System.out.println("recover....");
    }

    @Transactional(rollbackFor = Exception.class)
    public List<Role> getUserRoles(Long userId) {
        List<UserRole> userRoles = UserRole.F.where().userId.eq(userId).findList();
        List<Long> roleIds = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toList());
        if (CollectionUtils.isNotEmpty(roleIds)) {

            return Role.F.where().setIdIn(roleIds.toArray()).findList();
        }
        return new ArrayList<>();
    }

    @Transactional(rollbackFor = Exception.class)
    private boolean hasRole(Long userId, String roleName) {
        List<Role> roles = Lists.newArrayList();
        List<UserRole> userRoles = UserRole.F.where().userId.eq(userId).findList();
        List<Long> roleIds = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toList());
        if (CollectionUtils.isNotEmpty(roleIds)) {

            roles = Role.F.where().setIdIn(roleIds.toArray()).findList();
        }
        return roles.stream().anyMatch(role -> Objects.equals(role.getName(), roleName));
    }

    @Transactional(rollbackFor = Exception.class)
    private boolean hasPermission(Long userId, String permission) {
        List<Permission> permissions = Lists.newArrayList();
        List<UserRole> userRoles = UserRole.F.where().userId.eq(userId).findList();
        List<Long> roleIds = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toList());
        List<RolePermission> rolePermissions = RolePermission.F.where().roleId.in(roleIds.toArray(new Long[0])).findList();
        List<Long> permissionIds = rolePermissions.stream().map(RolePermission::getPermissionId).collect(Collectors.toList());
        if (CollectionUtils.isNotEmpty(permissionIds)) {

            permissions = Permission.F.where().setIdIn(permissionIds.toArray()).findList();
        }
        return permissions.stream().anyMatch(userPermission -> Objects.equals(userPermission.getPermission(), permission));
    }

}
